#pragma once
#include "point.hpp"
#include "agent.hpp"

#include <nlohmann/json.hpp>

#include <unordered_map>
#include <iostream>

namespace dw {
    class AgentManager
    {
    private:
        ID nNextID;
        uint32_t nAgentNum;

    public:
        std::unordered_map<ID, std::shared_ptr<dw::Agent>> Agents;

    public:
        /* Sigleton */
        AgentManager()
        {
            nNextID = 1;
            nAgentNum = 0;
        }

        // Ban copy
        AgentManager(const AgentManager &) = delete;

        ~AgentManager()
        {
            // done
        }

    private:
        ID getNextID()
        {
            return nNextID++;
        }

    public:

        std::unordered_map<ID, std::shared_ptr<Agent>>::iterator begin()
        {
            return Agents.begin();
        }

        std::unordered_map<ID, std::shared_ptr<Agent>>::const_iterator begin() const
        {
            return Agents.begin();
        }

        std::unordered_map<ID, std::shared_ptr<Agent>>::iterator end()
        {
            return Agents.end();
        }

        std::unordered_map<ID, std::shared_ptr<Agent>>::const_iterator end() const
        {
            return Agents.end();
        }

    public:
        // Create success: return id;
        // failed: return 0;
        #if 0
        ID CreateAgent(const Agent &agent)
        {
            ID id = getNextID();
            std::shared_ptr<Agent> new_agent = std::make_shared<Agent>(agent, id);
            pAgents.insert({id, std::move(new_agent)});
            return id;
        }
        #endif

        uint32_t AgentNum() const
        {
            return Agents.size();
        }

        bool Exists(ID id) {
            auto iter = Agents.find(id);
            return !(iter == Agents.end());
        }

        std::shared_ptr<dw::Agent> CreateAgent()
        {
            ID id = getNextID();
            std::shared_ptr<Agent> agent = std::make_shared<Agent>(id);
            Agents.insert({id, std::move(agent)});
            return Agents[id];
        }

        ID CreateAgent(const Agent::State &state)
        {
            ID id = getNextID();
            std::shared_ptr<Agent> agent = std::make_shared<Agent>(state, id);
            Agents.insert({id, std::move(agent)});
            return id;
        }

        bool UpdateAgentState(const Agent::State &state, ID id = 0)
        {
            if (!id) id = state.id;
            if (!Exists(id)) return false;

            Agents[id]->Update(state);
            return true;
        }

        std::shared_ptr<dw::AutonomousAgent> CreateAutonomousAgent()
        {
            ID id = getNextID();
            std::shared_ptr<AutonomousAgent> agent = std::make_shared<AutonomousAgent>(id);

            Agents.insert({id, agent});
            return agent;
        }

        bool DeleteAgent(ID id)
        {
            if (Exists(id)) {
                Agents[id].reset();
            }

            return Agents.erase(id);
        }

        Agent::State getAgentState(ID id)
        {
            if (Exists(id)) return Agents[id]->getAgentState();

            Agent::State state;
            state.vDirection = {0, 0};
            return state;
        }

        bool setAgentState(const Agent::State &state, ID id = 0)
        {
            if (!id) id = state.id;
            if (!Exists(id)) return false;

            Agents[id]->set(state, id);
            return true;
        }

        void Update(double fElapse)
        {
            for (auto pair : Agents)
            {
                pair.second->Update((float)fElapse);
            }
        }

        std::shared_ptr<dw::Agent> getAgent(ID id)
        {
            auto iter = Agents.find(id);
            if (Agents.end() == iter)
                return nullptr;
            return Agents[id];
        }

        ID load(const std::string &FilePath)
        {
            std::ifstream ifs(FilePath);
            if (!ifs.is_open())
                return false;

            std::string encoded;
            ifs >> encoded;
            ifs.close();

            return decode(encoded);
        }

        bool save(const std::string &FilePath)
        {
            std::ofstream ofs(FilePath);
            if (!ofs.is_open())
                return false;

            ofs << encode();
            ofs.close();
        }

        // id = 0: save all
        // else: encode specific agent according to id
        std::string encode()
        {
            nlohmann::json j;
            j["agents"] = nlohmann::json::array();
            to_json(j["agents"], *this);
            return j.dump();
        }

        bool decode(const std::string &encoded)
        {
            nlohmann::json j = nlohmann::json::parse(encoded);
            from_json(j["agents"], *this);
            return true;
        }

        friend void to_json(nlohmann::json &j, const AgentManager &agent_manager);
        friend void from_json(const nlohmann::json &j, AgentManager &agent_manager);

        friend std::ostream &operator<<(std::ostream &os, AgentManager &agent_manager)
        {
            os << agent_manager.encode();
            return os;
        }
    };

    void to_json(nlohmann::json &j, const AgentManager &agent_manager)
    {
        j = nlohmann::json::array();

        for (auto pair : agent_manager.Agents)
        {
            nlohmann::json agent;
            to_json(agent, *(pair.second));
            j += agent;
        }
    }

    void from_json(const nlohmann::json &j, AgentManager &agent_manager)
    {
        int cnt = 0;
        ID maxID = 0;
        for (auto agent : j)
        {
            Agent::State state;
            from_json(agent, state);
            if (!state) {
                agent_manager.Agents.erase(state.id);
                continue;
            }

            if (agent_manager.Exists(state.id)) {
                agent_manager.UpdateAgentState(state);
            }
            else {
                std::shared_ptr<Agent> p = std::make_shared<Agent>(state);
                agent_manager.Agents.insert({ state.id, std::move(p) });
            }
            cnt++;
            if (maxID < state.id) maxID = state.id;
        }
        agent_manager.nAgentNum = cnt;
        agent_manager.nNextID = maxID + 1;
    }
}